/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.web.portlet.visitor.impl; // NOPMD

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.mvc.Controller;
import org.springframework.web.servlet.support.JstlUtils;
import org.springframework.web.servlet.view.InternalResourceView;

import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.process.Comparators;
import com.genia.toolbox.web.io.HttpServletRequestProxy;
import com.genia.toolbox.web.io.HttpServletRequestProxyFactory;
import com.genia.toolbox.web.io.QueryableHttpServletResponse;
import com.genia.toolbox.web.portlet.bean.Portlet;
import com.genia.toolbox.web.portlet.bean.PortletView;
import com.genia.toolbox.web.portlet.bean.PortletViewFactory;
import com.genia.toolbox.web.portlet.bean.impl.AbstractLinkedPortlet;
import com.genia.toolbox.web.portlet.bean.impl.ContainerPortlet;
import com.genia.toolbox.web.portlet.bean.impl.DispatcherPortlet;
import com.genia.toolbox.web.portlet.bean.impl.HolePortlet;
import com.genia.toolbox.web.portlet.bean.impl.LinkedPortlet;
import com.genia.toolbox.web.portlet.bean.impl.SimplePortlet;
import com.genia.toolbox.web.portlet.bean.impl.TemplatePortlet;
import com.genia.toolbox.web.portlet.visitor.PortletVisitor;

/**
 * <code>PortletVisitor</code> that renders a <code>Portlet</code>.
 */
public class RenderingPortletVisitor
    implements PortletVisitor, BeanFactoryAware
{

  /**
   * the attribute key that define the sub portlets of a
   * <code>ContainerPortlet</code> for the rendering.
   */
  public static final String PORTLET_VIEWS_ATTRIBUTE_KEY = "com.genia.toolbox.web.portlet.visitor.impl.RenderingPortletVisitor.portletViews";

  /**
   * the attribute key that define the association beewteen Portlet Holes and
   * Portlet name for the current template.
   */
  public static final String PORTLET_HOLES_ATTRIBUTE_KEY = "com.genia.toolbox.web.portlet.visitor.impl.RenderingPortletVisitor.portletHoles";

  /**
   * the error i18n key when a <code>ContainerPortlet</code> ask for a
   * <code>View</code> that does not exist.
   */
  public static final String UNABLE_TO_FIND_VIEW_ERROR_KEY = "com.genia.toolbox.web.portlet.visitor.impl.RenderingPortletVisitor.unableToFindView";

  /**
   * the error i18n key when a <code>SimplePortlet</code> ask for a
   * <code>Controller</code> that does not exist.
   */
  public static final String UNABLE_TO_FIND_CONTROLLER_ERROR_KEY = "com.genia.toolbox.web.portlet.visitor.impl.RenderingPortletVisitor.unableToFindController";

  /**
   * the error i18n key when a <code>HolePortlet</code> is called within a
   * context that does not contain a TemplatePortlet that defines it.
   */
  public static final String UNABLE_TO_FIND_TEMPLATE_CONTEXT_ERROR_KEY = "com.genia.toolbox.web.portlet.visitor.impl.RenderingPortletVisitor.unableToFindTemplateContext";

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(RenderingPortletVisitor.class);

  /**
   * the output of the rendering of the <code>Portlet</code>.
   */
  private transient byte[] output;

  /**
   * the spring bean factory.
   */
  private transient ListableBeanFactory beanFactory;

  /**
   * the current HTTP request.
   */
  private HttpServletRequest request;

  /**
   * the current HTTP response.
   */
  private HttpServletResponse response;

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * the {@link MessageSource} to use.
   */
  private MessageSource messageSource;



  /**
   * getter for the output property.
   * 
   * @return the output
   */
  public byte[] getOutput()
  {
    return output;
  }



  /**
   * render a <code>ContainerPortlet</code>.
   * 
   * @param portlet
   *          the <code>ContainerPortlet</code> to render
   * @throws BundledException
   *           when an error occurs.
   */
  public void visitContainerPortlet(final ContainerPortlet portlet)
      throws BundledException
  {

    final HttpServletResponse oldResponse = response;
    final HttpServletRequest oldRequest = request;
    try {
      response = new QueryableHttpServletResponse(response);
      request = HttpServletRequestProxyFactory.getHttpServletRequestProxy(request);
      for (final Map.Entry<String, String> entry : portlet.getAttributes().entrySet()) {
        request.setAttribute(entry.getKey(), entry.getValue());
      }
      final Collection<PortletView> portletViews = new ArrayList<PortletView>();
      for (final Portlet subPortlet : portlet.getSubPortlets()) {
        subPortlet.visit(this);
        if (response.isCommitted()) {
          return;
        }
        final PortletView newPortletView = PortletViewFactory.newPortletView();
        newPortletView.setCharacterEncoding(response.getCharacterEncoding());
        newPortletView.setBytes(output);
        newPortletView.setIdentifier(subPortlet.getIdentifier());
        portletViews.add(newPortletView);
      }
      final View view = getView(portlet.getViewName());
      if (view == null) {
        throw new TechnicalException("Unable to find view", UNABLE_TO_FIND_VIEW_ERROR_KEY, portlet.getViewName());
      }
      final Map<String, Object> model = new HashMap<String, Object>();
      model.put(PORTLET_VIEWS_ATTRIBUTE_KEY, portletViews);
      fillModel(model, portlet);
      try {
        view.render(model, request, response);
        output = ((QueryableHttpServletResponse) response).getOutput();
      }
      catch (final Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }
    finally {
      response = oldResponse;
      request = oldRequest;
    }
  }



  /**
   * render a <code>DispatcherPortlet</code>.
   * 
   * @param portlet
   *          the <code>DispatcherPortlet</code> to render
   * @throws BundledException
   *           when an error occurs.
   */
  public void visitDispatcherPortlet(final DispatcherPortlet portlet)
      throws BundledException
  {
    final QueryableHttpServletResponse queryableResponse = new QueryableHttpServletResponse(response);
    final HttpServletRequestProxy requestProxy = HttpServletRequestProxyFactory.getHttpServletRequestProxy(request);
    for (final Map.Entry<String, String> entry : portlet.getAttributes().entrySet()) {
      requestProxy.setAttribute(entry.getKey(), entry.getValue());
    }
    try {
      InternalResourceView view = new InternalResourceView();
      view.setUrl(portlet.getUrl());
      JstlUtils.exposeLocalizationContext(requestProxy, getMessageSource());
      view.render(new HashMap<String, Object>(), requestProxy, queryableResponse);
      // requestProxy.getRequestDispatcher(portlet.getUrl()).forward(requestProxy,
      // queryableResponse);
      output = queryableResponse.getOutput();
    }
    catch (final Exception e) {
      throw getExceptionManager().convertException(e);
    }

  }



  /**
   * render a <code>SimplePortlet</code>.
   * 
   * @param portlet
   *          the <code>SimplePortlet</code> to render
   * @throws BundledException
   *           when an error occurs.
   */
  @SuppressWarnings("unchecked")
  public void visitSimplePortlet(final SimplePortlet portlet)
      throws BundledException
  {
    final QueryableHttpServletResponse queryableResponse = new QueryableHttpServletResponse(response);
    final HttpServletRequestProxy requestProxy = HttpServletRequestProxyFactory.getHttpServletRequestProxy(request);
    final Controller controller = (Controller) beanFactory.getBean(portlet.getControllerName());
    if (controller == null) {
      throw new TechnicalException("Unable to find controller", UNABLE_TO_FIND_CONTROLLER_ERROR_KEY, portlet.getControllerName());
    }
    for (final Map.Entry<String, String> entry : portlet.getParameters().entrySet()) {
      requestProxy.setParameter(entry.getKey(), entry.getValue());
    }
    try {
      final ModelAndView mav = controller.handleRequest(requestProxy, queryableResponse);
      if (mav != null) {
        final View view = getView(mav);
        if (view != null) {
          fillModel(mav.getModel(), portlet);
          view.render(mav.getModelMap(), requestProxy, queryableResponse);
        }
      }
      output = queryableResponse.getOutput();
    }
    catch (final Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * fill a model with the attributes of a <code>Portlet</code>.
   * 
   * @param model
   *          the model to fill
   * @param portlet
   *          the <code>Portlet</code> to get the attributes from
   */
  private void fillModel(final Map<String, Object> model, final Portlet portlet)
  {
    model.putAll(portlet.getAttributes());
  }



  /**
   * retrieve a <code>View</code> from a <code>ModelAndView</code>.
   * 
   * @param mav
   *          the <code>ModelAndView</code> to retrieve the view from
   * @return the <code>View</code> of the <code>ModelAndView</code>
   */
  private View getView(final ModelAndView mav)
  {
    if (mav.getView() != null) {
      return mav.getView();
    }
    return getView(mav.getViewName());
  }



  /**
   * retrieve a <code>View</code> by its name.
   * 
   * @param viewName
   *          the name of the <code>View</code>
   * @return the <code>View</code> identified by the name
   */
  @SuppressWarnings("unchecked")
  private View getView(final String viewName)
  {
    final List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>(beanFactory.getBeansOfType(ViewResolver.class).values());
    Collections.sort(viewResolvers, Comparators.ORDERED_COMPARATOR);
    for (final ViewResolver vr : viewResolvers) {
      View view = null;
      try {
        view = vr.resolveViewName(viewName, LocaleContextHolder.getLocale());
      }
      catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
      }
      if (view != null) {
        return view;
      }
    }
    return null;
  }



  /**
   * the visitor method for a <code>LinkedPortlet</code>.
   * 
   * @param portlet
   *          the <code>LinkedPortlet</code> begin visited
   * @throws BundledException
   *           if an error occurred
   */
  public void visitLinkedPortlet(final LinkedPortlet portlet)
      throws BundledException
  {
    visitAbstractLinkedPortlet(portlet);
  }



  /**
   * the visitor method for a <code>AbstractLinkedPortlet</code>.
   * 
   * @param portlet
   *          the <code>AbstractLinkedPortlet</code> begin visited
   * @throws BundledException
   *           if an error occurred
   */
  public void visitAbstractLinkedPortlet(final AbstractLinkedPortlet portlet)
      throws BundledException
  {
    // Let the subportlet handle this.
    portlet.getSubPortlet().visit(this);

  }



  /**
   * the visitor method for a <code>HolePortlet</code>.
   * 
   * @param portlet
   *          the <code>HolePortlet</code> begin visited
   * @throws BundledException
   *           if an error occurred
   */
  public void visitHolePortlet(final HolePortlet portlet)
      throws BundledException
  {
    @SuppressWarnings("unchecked")
    final Map<String, String> holesPortlets = (Map<String, String>) request.getAttribute(PORTLET_HOLES_ATTRIBUTE_KEY);
    final String linkedPortletName = holesPortlets.get(portlet.getHoleName());
    if (linkedPortletName == null) {
      throw new TechnicalException("Unable to find TemplatePortlet context", UNABLE_TO_FIND_TEMPLATE_CONTEXT_ERROR_KEY, portlet.getHoleName());
    }
    portlet.setCurrentPortletName(linkedPortletName);
    visitAbstractLinkedPortlet(portlet);
    // If the portlet has been called recursively (should not happen as this
    // should cause an infinite loop), we reset the portlet name.
    portlet.setCurrentPortletName(linkedPortletName);
  }



  /**
   * the visitor method for a <code>TemplatePortlet</code>.
   * 
   * @param portlet
   *          the <code>TemplatePortlet</code> begin visited
   * @throws BundledException
   *           if an error occurred
   */
  public void visitTemplatePortlet(final TemplatePortlet portlet)
      throws BundledException
  {
    final HttpServletRequest oldRequest = request;
    request = HttpServletRequestProxyFactory.getHttpServletRequestProxy(request);
    request.setAttribute(PORTLET_HOLES_ATTRIBUTE_KEY, portlet.getHolesPortlets());
    visitAbstractLinkedPortlet(portlet);
    request = oldRequest;
  }



  /**
   * Callback that supplies the owning factory to a bean instance.
   * <p>
   * Invoked after the population of normal bean properties but before an
   * initialization callback such as
   * {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
   * or a custom init-method.
   * 
   * @param beanFactory
   *          owning BeanFactory (never <code>null</code>). The bean can
   *          immediately call methods on the factory.
   * @see org.springframework.beans.factory.BeanInitializationException
   * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
   */
  public void setBeanFactory(BeanFactory beanFactory)
  {
    this.beanFactory = (ListableBeanFactory) beanFactory;
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * getter for the request property.
   * 
   * @return the request
   */
  public HttpServletRequest getRequest()
  {
    return request;
  }



  /**
   * setter for the request property.
   * 
   * @param request
   *          the request to set
   */
  public void setRequest(HttpServletRequest request)
  {
    this.request = request;
  }



  /**
   * getter for the response property.
   * 
   * @return the response
   */
  public HttpServletResponse getResponse()
  {
    return response;
  }



  /**
   * setter for the response property.
   * 
   * @param response
   *          the response to set
   */
  public void setResponse(HttpServletResponse response)
  {
    this.response = response;
  }



  /**
   * getter for the messageSource property.
   * 
   * @return the messageSource
   */
  public MessageSource getMessageSource()
  {
    return messageSource;
  }



  /**
   * setter for the messageSource property.
   * 
   * @param messageSource
   *          the messageSource to set
   */
  public void setMessageSource(MessageSource messageSource)
  {
    this.messageSource = messageSource;
  }

}
